1 /**
2 Copyright: Copyright (c) 2016-2021, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 
10 Utility functions for Clang Compilation Databases.
11 
12 # Usage
13 Call the function `fromArgCompileDb` to create one, merged database.
14 
15 Extract flags the flags for a file by calling `appendOrError`.
16 
17 Example:
18 ---
19 auto dbs = fromArgCompileDb(["foo.json]);
20 auto flags = dbs.appendOrError(dbs, null, "foo.cpp", defaultCompilerFlagFilter);
21 ---
22 */
23 module compile_db;
24 
25 import logger = std.experimental.logger;
26 import std.algorithm : map, filter, splitter, joiner;
27 import std.array : empty, array, appender;
28 import std.exception : collectException;
29 import std.json : JSONValue;
30 import std.path : buildPath;
31 import std.typecons : Nullable;
32 
33 import my.path : AbsolutePath, Path;
34 
35 public import compile_db.user_filerange;
36 public import compile_db.system_compiler : deduceSystemIncludes, SystemIncludePath, Compiler;
37 
38 package void shouldEqual(T0, T1)(T0 a, T1 b) {
39     import std.stdio : writeln;
40 
41     if (a != b) {
42         writeln(a, " != ", b);
43         assert(0);
44     }
45 }
46 
47 package void shouldBeIn(T0, T1)(T0 a, T1 b) {
48     import std.stdio : writeln;
49 
50     bool found;
51     foreach (v; b) {
52         if (a == v) {
53             found = true;
54             break;
55         }
56     }
57 
58     if (!found) {
59         writeln(a, " not found in ", b);
60         assert(0);
61     }
62 }
63 
64 @safe:
65 
66 /** Hold an entry from the compilation database.
67  *
68  * The following information is from the official specification.
69  * $(LINK2 http://clang.llvm.org/docs/JSONCompilationDatabase.html, Standard)
70  *
71  * directory: The working directory of the compilation. All paths specified in
72  * the command or file fields must be either absolute or relative to this
73  * directory.
74  *
75  * file: The main translation unit source processed by this compilation step.
76  * This is used by tools as the key into the compilation database. There can be
77  * multiple command objects for the same file, for example if the same source
78  * file is compiled with different configurations.
79  *
80  * command: The compile command executed. After JSON unescaping, this must be a
81  * valid command to rerun the exact compilation step for the translation unit
82  * in the environment the build system uses. Parameters use shell quoting and
83  * shell escaping of quotes, with ‘"‘ and ‘\‘ being the only special
84  * characters. Shell expansion is not supported.
85  *
86  * argumets: The compile command executed as list of strings. Either arguments
87  * or command is required.
88  *
89  * output: The name of the output created by this compilation step. This field
90  * is optional. It can be used to distinguish different processing modes of the
91  * same input file.
92  *
93  * Additions.
94  * The standard do not specify how to treat "directory" when it is a relative
95  * path. The logic chosen in dextool is to treat it as relative to the path
96  * the compilation database file is read from.
97  */
98 struct CompileCommand {
99     /// The raw command from the tuples "command" or "arguments value.
100     static struct Command {
101         string[] payload;
102         alias payload this;
103         bool hasValue() @safe pure nothrow const @nogc {
104             return payload.length != 0;
105         }
106     }
107 
108     /// File that where compiled.
109     Path file;
110     /// ditto.
111     AbsolutePath absoluteFile;
112     /// Working directory of the command that compiled the input.
113     AbsolutePath directory;
114     /// The executing command when compiling.
115     Command command;
116     /// The resulting object file.
117     Path output;
118     /// ditto.
119     AbsolutePath absoluteOutput;
120 }
121 
122 /// The path to the compilation database.
123 struct CompileDbFile {
124     Path payload;
125     alias payload this;
126 
127     this(string p) @safe nothrow {
128         payload = Path(p);
129     }
130 }
131 
132 /// The absolute path to the directory the compilation database reside at.
133 struct AbsoluteCompileDbDirectory {
134     AbsolutePath payload;
135     alias payload this;
136 
137     this(Path path) {
138         import std.path : dirName;
139 
140         payload = AbsolutePath(path.dirName.Path);
141     }
142 }
143 
144 /// A complete compilation database.
145 struct CompileCommandDB {
146     CompileCommand[] payload;
147     alias payload this;
148 
149     bool empty() @safe pure nothrow const @nogc {
150         return payload.empty;
151     }
152 }
153 
154 // The result of searching for a file in a compilation DB.
155 // The file may be occur more than one time therefor an array.
156 struct CompileCommandSearch {
157     CompileCommand[] payload;
158     alias payload this;
159 
160     bool empty() @safe pure nothrow const @nogc {
161         return payload.empty;
162     }
163 }
164 
165 /**
166  * Trusted: opIndex for JSONValue is @safe in DMD-2.077.0
167  * remove the trusted attribute when the minimal requirement is upgraded.
168  */
169 private Nullable!CompileCommand toCompileCommand(JSONValue v, AbsoluteCompileDbDirectory db_dir) nothrow @trusted {
170     import std.exception : assumeUnique;
171     import std.range : only;
172     import std.utf : byUTF;
173 
174     static if (__VERSION__ < 2085L) {
175         import std.json : JSON_TYPE;
176 
177         alias JSONType = JSON_TYPE;
178         alias JSONType_array = JSON_TYPE.ARRAY;
179         alias JSONType_string = JSON_TYPE.STRING;
180     } else {
181         import std.json : JSONType;
182 
183         alias JSONType_array = JSONType.array;
184         alias JSONType_string = JSONType..string;
185     }
186 
187     string[] command = () {
188         string[] cmd;
189         try {
190             cmd = v["command"].str.splitter.filter!(a => a.length != 0).array;
191         } catch (Exception ex) {
192         }
193 
194         // prefer command over arguments if both are present because of bugs in
195         // tools that produce compile_commands.json.
196         if (cmd.length != 0)
197             return cmd;
198 
199         try {
200             enum j_arg = "arguments";
201             const auto j_type = v[j_arg].type;
202             if (j_type == JSONType_string)
203                 cmd = v[j_arg].str.splitter.filter!(a => a.length != 0).array;
204             else if (j_type == JSONType_array) {
205                 import std.range;
206 
207                 cmd = v[j_arg].arrayNoRef
208                     .filter!(a => a.type == JSONType_string)
209                     .map!(a => a.str)
210                     .filter!(a => a.length != 0)
211                     .array;
212             }
213         } catch (Exception ex) {
214         }
215 
216         return cmd;
217     }();
218 
219     if (command.length == 0) {
220         logger.error("Unable to parse the JSON tuple. Both command and arguments are empty")
221             .collectException;
222         return typeof(return)();
223     }
224 
225     string output;
226     try {
227         output = v["output"].str;
228     } catch (Exception ex) {
229     }
230 
231     try {
232         const directory = v["directory"];
233         const file = v["file"];
234 
235         foreach (a; only(directory, file).map!(a => !a.isNull && a.type == JSONType_string)
236                 .filter!(a => !a)) {
237             // sanity check.
238             // if any element is false then break early.
239             return typeof(return)();
240         }
241 
242         return toCompileCommand(directory.str, file.str, command, db_dir, output);
243     } catch (Exception e) {
244         logger.info("Input JSON: ", v.toPrettyString).collectException;
245         logger.error("Unable to parse json: ", e.msg).collectException;
246     }
247 
248     return typeof(return)();
249 }
250 
251 /** Transform a json entry to a CompileCommand.
252  *
253  * This function is under no circumstances meant to be exposed outside this module.
254  * The API is badly designed for common use because it relies on the position
255  * order of the strings for their meaning.
256  */
257 Nullable!CompileCommand toCompileCommand(string directory, string file,
258         string[] command, AbsoluteCompileDbDirectory db_dir, string output) nothrow {
259     // expects that v is a tuple of 3 json values with the keys directory,
260     // command, file
261 
262     Nullable!CompileCommand rval;
263 
264     try {
265         auto abs_workdir = AbsolutePath(buildPath(db_dir, directory.Path));
266         auto abs_file = AbsolutePath(buildPath(abs_workdir, file.Path));
267         auto abs_output = AbsolutePath(buildPath(abs_workdir, output.Path));
268         // dfmt off
269         rval = CompileCommand(
270             Path(file),
271             abs_file,
272             abs_workdir,
273             CompileCommand.Command(command),
274             Path(output),
275             abs_output);
276         // dfmt on
277     } catch (Exception ex) {
278         logger.error("Unable to parse json: ", ex.msg).collectException;
279     }
280 
281     return rval;
282 }
283 
284 /** Parse a CompilationDatabase.
285  *
286  * Params:
287  *  raw_input = the content of the CompilationDatabase.
288  *  db = path to the compilation database file.
289  *  out_range = range to write the output to.
290  */
291 private void parseCommands(T)(string raw_input, CompileDbFile db, ref T out_range) nothrow {
292     import std.json : parseJSON, JSONException;
293 
294     static void put(T)(JSONValue v, AbsoluteCompileDbDirectory dbdir, ref T out_range) nothrow {
295 
296         try {
297             // dfmt off
298             foreach (e; v.array()
299                      // map the JSON tuples to D structs
300                      .map!(a => toCompileCommand(a, dbdir))
301                      // remove invalid
302                      .filter!(a => !a.isNull)
303                      .map!(a => a.get)) {
304                 out_range.put(e);
305             }
306             // dfmt on
307         } catch (Exception ex) {
308             logger.error("Unable to parse json:", ex.msg).collectException;
309         }
310     }
311 
312     try {
313         // trusted: is@safe in DMD-2.077.0
314         // remove the trusted attribute when the minimal requirement is upgraded.
315         auto json = () @trusted { return parseJSON(raw_input); }();
316         auto as_dir = AbsoluteCompileDbDirectory(db.AbsolutePath);
317 
318         // trusted: this function is private so the only user of it is this module.
319         // the only problem would be in the out_range. It is assumed that the
320         // out_range takes care of the validation and other security aspects.
321         () @trusted { put(json, as_dir, out_range); }();
322     } catch (Exception ex) {
323         logger.error("Error while parsing compilation database: " ~ ex.msg).collectException;
324     }
325 }
326 
327 void fromFile(T)(CompileDbFile filename, ref T app) {
328     import std.file : readText;
329 
330     auto raw = readText(filename);
331     if (raw.length == 0)
332         logger.warning("File is empty: ", filename);
333 
334     raw.parseCommands(filename, app);
335 }
336 
337 void fromFiles(T)(CompileDbFile[] fnames, ref T app) {
338     import std.file : exists;
339 
340     foreach (f; fnames) {
341         if (!exists(f))
342             throw new Exception("File do not exist: " ~ f);
343         f.fromFile(app);
344     }
345 }
346 
347 /** Return default path if argument is null.
348  */
349 CompileDbFile[] orDefaultDb(string[] cli_path) @safe nothrow {
350     if (cli_path.length == 0) {
351         return [CompileDbFile("compile_commands.json")];
352     }
353 
354     return cli_path.map!(a => CompileDbFile(a)).array();
355 }
356 
357 /** Find a best matching compile_command in the database against the path
358  * pattern `glob`.
359  *
360  * When searching for the compile command for a file, the compilation db can
361  * return several commands, as the file may have been compiled with different
362  * options in different parts of the project.
363  *
364  * Params:
365  *  glob = glob pattern to find a matching file in the DB against
366  */
367 CompileCommandSearch find(CompileCommandDB db, string glob) @safe {
368     foreach (a; db.filter!(a => isMatch(a, glob))) {
369         return CompileCommandSearch([a]);
370     }
371     return CompileCommandSearch.init;
372 }
373 
374 /** Check if `glob` fuzzy matches `a`.
375  */
376 bool isMatch(CompileCommand a, string glob) {
377     import std.path : globMatch;
378 
379     if (a.absoluteFile == glob)
380         return true;
381     else if (a.absoluteFile == AbsolutePath(glob))
382         return true;
383     else if (a.file == glob)
384         return true;
385     else if (globMatch(a.absoluteFile, glob))
386         return true;
387     else if (a.absoluteOutput == glob)
388         return true;
389     else if (a.output == glob)
390         return true;
391     else if (globMatch(a.absoluteOutput, glob))
392         return true;
393     return false;
394 }
395 
396 string toString(CompileCommand[] db) @safe pure {
397     import std.conv : text;
398     import std.format : formattedWrite;
399 
400     auto app = appender!string();
401 
402     foreach (a; db) {
403         formattedWrite(app, "%s\n  %s\n  %s\n", a.directory, a.file, a.absoluteFile);
404 
405         if (!a.output.empty) {
406             formattedWrite(app, "  %s\n", a.output);
407             formattedWrite(app, "  %s\n", a.absoluteOutput);
408         }
409 
410         if (!a.command.empty)
411             formattedWrite(app, "  %-(%s %)\n", a.command);
412     }
413 
414     return app.data;
415 }
416 
417 string toString(CompileCommandDB db) @safe pure {
418     return toString(db.payload);
419 }
420 
421 string toString(CompileCommandSearch search) @safe pure {
422     return toString(search.payload);
423 }
424 
425 CompileCommandFilter defaultCompilerFilter() {
426     return CompileCommandFilter(defaultCompilerFlagFilter, 0);
427 }
428 
429 /// Returns: array of default flags to exclude.
430 auto defaultCompilerFlagFilter() @safe {
431     auto app = appender!(FilterClangFlag[])();
432 
433     // dfmt off
434     foreach (f; [
435              // remove basic compile flag irrelevant for AST generation
436              "-c", "-o",
437              // machine dependent flags
438              "-m",
439              // machine dependent flags, AVR
440              "-nodevicelib", "-Waddr-space-convert",
441              // machine dependent flags, VxWorks
442              "-non-static", "-Bstatic", "-Bdynamic", "-Xbind-lazy", "-Xbind-now",
443              // blacklist all -f because most aren not compatible with clang
444              "-f",
445              // linker flags, irrelevant for the AST
446              "-static", "-shared", "-rdynamic", "-s", "-l", "-L", "-z", "-u", "-T", "-Xlinker",
447              // a linker flag with filename as one argument
448              "-l",
449              // remove some of the preprocessor flags, irrelevant for the AST
450              "-MT", "-MF", "-MD", "-MQ", "-MMD", "-MP", "-MG", "-E", "-cc1", "-S", "-M", "-MM", "-###",
451              ]) {
452         app.put(FilterClangFlag(f));
453     }
454     // dfmt on
455 
456     return app.data;
457 }
458 
459 struct CompileCommandFilter {
460     FilterClangFlag[] filter;
461     int skipCompilerArgs = 0;
462 }
463 
464 /// Parsed compiler flags.
465 struct ParseFlags {
466     /// The includes used in the compile command
467     static struct Include {
468         string payload;
469         alias payload this;
470     }
471 
472     private {
473         bool forceSystemIncludes_;
474     }
475 
476     /// The includes used in the compile command.
477     Include[] includes;
478 
479     /// System include paths extracted from the compiler used for the file.
480     SystemIncludePath[] systemIncludes;
481 
482     /// Specific flags for the file as parsed from the DB.
483     string[] cflags;
484 
485     /// Compiler used to compile the item.
486     Compiler compiler;
487 
488     void prependCflags(string[] v) {
489         this.cflags = v ~ this.cflags;
490     }
491 
492     void appendCflags(string[] v) {
493         this.cflags ~= v;
494     }
495 
496     /// Set to true to use -I instead of -isystem for system includes.
497     auto forceSystemIncludes(bool v) {
498         this.forceSystemIncludes_ = v;
499         return this;
500     }
501 
502     bool hasSystemIncludes() @safe pure nothrow const @nogc {
503         return systemIncludes.length != 0;
504     }
505 
506     string toString() @safe pure const {
507         import std.format : format;
508 
509         return format("Compiler:%s flags: %-(%s %)", compiler, completeFlags);
510     }
511 
512     /** Easy to use method that has the complete flags ready to use with a GCC
513      * complient compiler.
514      *
515      * This method assumes that -isystem is how to add system flags.
516      *
517      * Returns: flags with the system flags appended.
518      */
519     string[] completeFlags() @safe pure nothrow const {
520         auto incl_param = forceSystemIncludes_ ? "-I" : "-isystem";
521 
522         return cflags.idup ~ systemIncludes.map!(a => [incl_param, a.value]).joiner.array;
523     }
524 
525     alias completeFlags this;
526 
527     this(Include[] incls, string[] flags) {
528         this(Compiler.init, incls, SystemIncludePath[].init, flags);
529     }
530 
531     this(Compiler compiler, Include[] incls, string[] flags) {
532         this(compiler, incls, null, flags);
533     }
534 
535     this(Compiler compiler, Include[] incls, SystemIncludePath[] sysincls, string[] flags) {
536         this.compiler = compiler;
537         this.includes = incls;
538         this.systemIncludes = sysincls;
539         this.cflags = flags;
540     }
541 }
542 
543 /** Filter and normalize the compiler flags.
544  *
545  *  - Sanitize the compiler command by removing flags matching the filter.
546  *  - Remove excess white space.
547  *  - Convert all filenames to absolute path.
548  */
549 ParseFlags parseFlag(CompileCommand cmd, const CompileCommandFilter flag_filter) @safe {
550     import std.algorithm : among, strip, startsWith, count;
551     import std..string : empty, split;
552 
553     static bool excludeStartWith(const string raw_flag, const FilterClangFlag[] flag_filter) @safe {
554         // the purpuse is to find if any of the flags in flag_filter matches
555         // the start of flag.
556 
557         bool delegate(const FilterClangFlag) @safe cmp;
558 
559         const parts = raw_flag.split('=');
560         if (parts.length == 2) {
561             // is a -foo=bar flag thus exact match is the only sensible
562             cmp = (const FilterClangFlag a) => raw_flag == a.payload;
563         } else {
564             // the flag has the argument merged thus have to check if the start match
565             cmp = (const FilterClangFlag a) => raw_flag.startsWith(a.payload);
566         }
567 
568         // dfmt off
569         return 0 != flag_filter
570             .filter!(a => a.kind == FilterClangFlag.Kind.exclude)
571             // keep flags that are at least the length of values
572             .filter!(a => raw_flag.length >= a.length)
573             // if the flag is any of those in filter
574             .filter!cmp
575             .count();
576         // dfmt on
577     }
578 
579     static bool isQuotationMark(char c) @safe {
580         return c == '"';
581     }
582 
583     static bool isBackslash(char c) @safe {
584         return c == '\\';
585     }
586 
587     static bool isInclude(string flag) @safe {
588         return flag.length >= 2 && flag[0 .. 2] == "-I";
589     }
590 
591     static bool isCombinedIncludeFlag(string flag) @safe {
592         // if an include flag make it absolute, as one argument by checking
593         // length. 3 is to only match those that are -Ixyz
594         return flag.length >= 3 && isInclude(flag);
595     }
596 
597     static bool isNotAFlag(string flag) @safe {
598         // good enough if it seem to be a file
599         return flag.length >= 1 && flag[0] != '-';
600     }
601 
602     /// Flags that take an argument that is a path that need to be transformed
603     /// to an absolute path.
604     static bool isFlagAndPath(string flag) @safe {
605         // list derived from clang --help
606         return 0 != flag.among("-I", "-idirafter", "-iframework", "-imacros", "-include-pch",
607                 "-include", "-iquote", "-isysroot", "-isystem-after", "-isystem", "--sysroot");
608     }
609 
610     /// Flags that take an argument that is NOT a path.
611     static bool isFlagAndValue(string flag) @safe {
612         return 0 != flag.among("-D");
613     }
614 
615     /// Flags that are includes, but contains spaces, are wrapped in quotation marks (or slash).
616     static bool isIncludeWithQuotationMark(string flag) @safe {
617         // length is checked in isCombinedIncludeFlag
618         return isCombinedIncludeFlag(flag) && (isQuotationMark(flag[2]) || isBackslash(flag[2]));
619     }
620 
621     /// Flags that are paths and contain spaces will start with a quotation mark (or slash).
622     static bool isStartingWithQuotationMark(string flag) @safe {
623         return !flag.empty && (isQuotationMark(flag[0]) || isBackslash(flag[0]));
624     }
625 
626     /// When we know we are building a path that is space separated,
627     /// the last index of the last string should be a quotation mark.
628     static bool isEndingWithQuotationMark(string flag) @safe {
629         return !flag.empty && isQuotationMark(flag[$ - 1]);
630     }
631 
632     static ParseFlags filterPair(string[] r, AbsolutePath workdir,
633             const FilterClangFlag[] flag_filter) @safe {
634         enum State {
635             /// keep the next flag IF none of the other transitions happens
636             keep,
637             /// forcefully keep the next argument as raw data
638             priorityKeepNextArg,
639             /// keep the next argument and transform to an absolute path
640             pathArgumentToAbsolute,
641             /// skip the next arg
642             skip,
643             /// skip the next arg, if it is not a flag
644             skipIfNotFlag,
645             /// use the next arg to create a complete path
646             checkingForEndQuotation,
647         }
648 
649         auto st = State.keep;
650         auto rval = appender!(string[]);
651         auto includes = appender!(string[]);
652         auto compiler = Compiler(r.length == 0 ? null : r[0]);
653         auto path = appender!(char[])();
654 
655         string removeBackslashesAndQuotes(string arg) {
656             import std.conv : text;
657             import std.uni : byCodePoint, byGrapheme, Grapheme;
658 
659             return arg.byGrapheme.filter!(a => !a.among(Grapheme('\\'),
660                     Grapheme('"'))).byCodePoint.text;
661         }
662 
663         void putNormalizedAbsolute(string arg) {
664             import std.path : buildNormalizedPath, absolutePath;
665 
666             auto p = buildNormalizedPath(workdir, removeBackslashesAndQuotes(arg)).absolutePath;
667             rval.put(p);
668             includes.put(p);
669         }
670 
671         foreach (arg; r) {
672             // First states and how to handle those.
673             // Then transitions from the state keep, which is the default state.
674             //
675             // The user controlled excludeStartWith must be before any other
676             // conditions after the states. It is to give the user the ability
677             // to filter out any flag.
678 
679             if (st == State.skip) {
680                 st = State.keep;
681             } else if (st == State.skipIfNotFlag && isNotAFlag(arg)) {
682                 st = State.keep;
683             } else if (st == State.pathArgumentToAbsolute) {
684                 if (isStartingWithQuotationMark(arg)) {
685                     if (isEndingWithQuotationMark(arg)) {
686                         st = State.keep;
687                         putNormalizedAbsolute(arg);
688                     } else {
689                         st = State.checkingForEndQuotation;
690                         path.put(arg);
691                     }
692                 } else {
693                     st = State.keep;
694                     putNormalizedAbsolute(arg);
695                 }
696             } else if (st == State.priorityKeepNextArg) {
697                 st = State.keep;
698                 rval.put(arg);
699             } else if (st == State.checkingForEndQuotation) {
700                 path.put(" ");
701                 path.put(arg);
702                 if (isEndingWithQuotationMark(arg)) {
703                     // the end of a divided path
704                     st = State.keep;
705                     putNormalizedAbsolute(path.data.idup);
706                     path.clear;
707                 }
708             } else if (excludeStartWith(arg, flag_filter)) {
709                 st = State.skipIfNotFlag;
710             } else if (isIncludeWithQuotationMark(arg)) {
711                 rval.put("-I");
712                 if (arg.length >= 4) {
713                     if (isEndingWithQuotationMark(arg)) {
714                         // the path is wrapped in quotes (ex ['-I"path/to src"'] or ['-I\"path/to src\"'])
715                         putNormalizedAbsolute(arg[2 .. $]);
716                     } else {
717                         // the path is divided (ex ['-I"path/to', 'src"'] or ['-I\"path/to', 'src\"'])
718                         st = State.checkingForEndQuotation;
719                         path.put(arg[2 .. $]);
720                     }
721                 }
722             } else if (isCombinedIncludeFlag(arg)) {
723                 rval.put("-I");
724                 putNormalizedAbsolute(arg[2 .. $]);
725             } else if (isFlagAndPath(arg)) {
726                 rval.put(arg);
727                 st = State.pathArgumentToAbsolute;
728             } else if (isFlagAndValue(arg)) {
729                 rval.put(arg);
730                 st = State.priorityKeepNextArg;
731             }  // parameter that seem to be filenames, remove
732             else if (isNotAFlag(arg)) {
733                 // skipping
734             } else {
735                 rval.put(arg);
736             }
737         }
738         return ParseFlags(compiler, includes.data.map!(a => ParseFlags.Include(a)).array, rval.data);
739     }
740 
741     import std.algorithm : min;
742 
743     string[] skipArgs = () @safe {
744         string[] args;
745         if (cmd.command.hasValue)
746             args = cmd.command.payload.dup;
747         if (args.length > flag_filter.skipCompilerArgs && flag_filter.skipCompilerArgs != 0)
748             args = args[min(flag_filter.skipCompilerArgs, args.length) .. $];
749         return args;
750     }();
751 
752     auto pargs = filterPair(skipArgs, cmd.directory, flag_filter.filter);
753 
754     return ParseFlags(pargs.compiler, pargs.includes, null, pargs.cflags);
755 }
756 
757 /** Convert the string to a CompileCommandDB.
758  *
759  * Params:
760  * path = changes relative paths to be relative this parameter
761  * data = input to convert
762  */
763 CompileCommandDB toCompileCommandDB(string data, Path path) @safe {
764     auto app = appender!(CompileCommand[])();
765     data.parseCommands(CompileDbFile(cast(string) path), app);
766     return CompileCommandDB(app.data);
767 }
768 
769 CompileCommandDB fromArgCompileDb(AbsolutePath[] paths) @safe {
770     return fromArgCompileDb(paths.map!(a => cast(string) a).array);
771 }
772 
773 /// Import and merge many compilation databases into one DB.
774 CompileCommandDB fromArgCompileDb(string[] paths) @safe {
775     auto app = appender!(CompileCommand[])();
776     paths.orDefaultDb.fromFiles(app);
777 
778     return CompileCommandDB(app.data);
779 }
780 
781 /// Flags to exclude from the flags passed on to the clang parser.
782 struct FilterClangFlag {
783     string payload;
784     alias payload this;
785 
786     enum Kind {
787         exclude
788     }
789 
790     Kind kind;
791 }
792 
793 @("Should be cflags with all unnecessary flags removed")
794 unittest {
795     auto cmd = toCompileCommand("/home", "file1.cpp", [
796             "g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun", "-c",
797             "a_filename.c"
798             ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null);
799     auto s = cmd.get.parseFlag(defaultCompilerFilter);
800     s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]);
801     s.includes.shouldEqual(["/home/bar", "/home/gun"]);
802 }
803 
804 @("Should be cflags with some excess spacing")
805 unittest {
806     auto cmd = toCompileCommand("/home", "file1.cpp", [
807             "g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun"
808             ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null);
809 
810     auto s = cmd.get.parseFlag(defaultCompilerFilter);
811     s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]);
812     s.includes.shouldEqual(["/home/bar", "/home/gun"]);
813 }
814 
815 @("Should be cflags with machine dependent removed")
816 unittest {
817     auto cmd = toCompileCommand("/home", "file1.cpp", [
818             "g++", "-mfoo", "-m", "bar", "-MD", "-lfoo.a", "-l", "bar.a", "-I",
819             "bar", "-Igun", "-c", "a_filename.c"
820             ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null);
821 
822     auto s = cmd.get.parseFlag(defaultCompilerFilter);
823     s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]);
824     s.includes.shouldEqual(["/home/bar", "/home/gun"]);
825 }
826 
827 @("Should be cflags with all -f removed")
828 unittest {
829     auto cmd = toCompileCommand("/home", "file1.cpp", [
830             "g++", "-fmany-fooo", "-I", "bar", "-fno-fooo", "-Igun", "-flolol",
831             "-c", "a_filename.c"
832             ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null);
833 
834     auto s = cmd.get.parseFlag(defaultCompilerFilter);
835     s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]);
836     s.includes.shouldEqual(["/home/bar", "/home/gun"]);
837 }
838 
839 @("shall NOT remove -std=xyz flags")
840 unittest {
841     auto cmd = toCompileCommand("/home", "file1.cpp", [
842             "g++", "-std=c++11", "-c", "a_filename.c"
843             ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null);
844 
845     auto s = cmd.get.parseFlag(defaultCompilerFilter);
846     s.cflags.shouldEqual(["-std=c++11"]);
847 }
848 
849 @("shall remove -mfloat-gprs=double")
850 unittest {
851     auto cmd = toCompileCommand("/home", "file1.cpp", [
852             "g++", "-std=c++11", "-mfloat-gprs=double", "-c", "a_filename.c"
853             ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null);
854     auto my_filter = CompileCommandFilter(defaultCompilerFlagFilter, 0);
855     my_filter.filter ~= FilterClangFlag("-mfloat-gprs=double", FilterClangFlag.Kind.exclude);
856     auto s = cmd.get.parseFlag(my_filter);
857     s.cflags.shouldEqual(["-std=c++11"]);
858 }
859 
860 @("Shall keep all compiler flags as they are")
861 unittest {
862     auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-Da", "-D",
863             "b"], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null);
864 
865     auto s = cmd.get.parseFlag(defaultCompilerFilter);
866     s.cflags.shouldEqual(["-Da", "-D", "b"]);
867 }
868 
869 version (unittest) {
870     import std.file : getcwd;
871     import std.path : absolutePath;
872     import std.format : format;
873 
874     // contains a bit of extra junk that is expected to be removed
875     immutable string dummy_path = "/path/to/../to/./db/compilation_db.json";
876     immutable string dummy_dir = "/path/to/db";
877 
878     enum raw_dummy1 = `[
879     {
880         "directory": "dir1/dir2",
881         "command": "g++ -Idir1 -c -o binary file1.cpp",
882         "file": "file1.cpp"
883     }
884 ]`;
885 
886     enum raw_dummy2 = `[
887     {
888         "directory": "dir",
889         "command": "g++ -Idir1 -c -o binary file1.cpp",
890         "file": "file1.cpp"
891     },
892     {
893         "directory": "dir",
894         "command": "g++ -Idir1 -c -o binary file2.cpp",
895         "file": "file2.cpp"
896     }
897 ]`;
898 
899     enum raw_dummy3 = `[
900     {
901         "directory": "dir1",
902         "command": "g++ -Idir1 -c -o binary file3.cpp",
903         "file": "file3.cpp"
904     },
905     {
906         "directory": "dir2",
907         "command": "g++ -Idir1 -c -o binary file3.cpp",
908         "file": "file3.cpp"
909     }
910 ]`;
911 
912     enum raw_dummy4 = `[
913     {
914         "directory": "dir1",
915         "arguments": "g++ -Idir1 -c -o binary file3.cpp",
916         "file": "file3.cpp",
917         "output": "file3.o"
918     },
919     {
920         "directory": "dir2",
921         "arguments": "g++ -Idir1 -c -o binary file3.cpp",
922         "file": "file3.cpp",
923         "output": "file3.o"
924     }
925 ]`;
926 
927     enum raw_dummy5 = `[
928     {
929         "directory": "dir1",
930         "arguments": ["g++", "-Idir1", "-c", "-o", "binary", "file3.cpp"],
931         "file": "file3.cpp",
932         "output": "file3.o"
933     },
934     {
935         "directory": "dir2",
936         "arguments": ["g++", "-Idir1", "-c", "-o", "binary", "file3.cpp"],
937         "file": "file3.cpp",
938         "output": "file3.o"
939     }
940 ]`;
941 }
942 
943 @("Should be a compile command DB")
944 unittest {
945     auto app = appender!(CompileCommand[])();
946     raw_dummy1.parseCommands(CompileDbFile(dummy_path), app);
947     auto cmds = app.data;
948 
949     assert(cmds.length == 1);
950     (cast(string) cmds[0].directory).shouldEqual(dummy_dir ~ "/dir1/dir2");
951     cmds[0].command.shouldEqual([
952             "g++", "-Idir1", "-c", "-o", "binary", "file1.cpp"
953             ]);
954     (cast(string) cmds[0].file).shouldEqual("file1.cpp");
955     (cast(string) cmds[0].absoluteFile).shouldEqual(dummy_dir ~ "/dir1/dir2/file1.cpp");
956 }
957 
958 @("Should be a DB with two entries")
959 unittest {
960     auto app = appender!(CompileCommand[])();
961     raw_dummy2.parseCommands(CompileDbFile(dummy_path), app);
962     auto cmds = app.data;
963 
964     (cast(string) cmds[0].file).shouldEqual("file1.cpp");
965     (cast(string) cmds[1].file).shouldEqual("file2.cpp");
966 }
967 
968 @("Should find filename")
969 unittest {
970     auto app = appender!(CompileCommand[])();
971     raw_dummy2.parseCommands(CompileDbFile(dummy_path), app);
972     auto cmds = CompileCommandDB(app.data);
973 
974     auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp");
975     assert(found.length == 1);
976     (cast(string) found[0].file).shouldEqual("file2.cpp");
977 }
978 
979 @("Should find no match by using an absolute path that doesn't exist in DB")
980 unittest {
981     auto app = appender!(CompileCommand[])();
982     raw_dummy2.parseCommands(CompileDbFile(dummy_path), app);
983     auto cmds = CompileCommandDB(app.data);
984 
985     auto found = cmds.find("./file2.cpp");
986     assert(found.length == 0);
987 }
988 
989 @("Should find one match by using the absolute filename to disambiguous")
990 unittest {
991     auto app = appender!(CompileCommand[])();
992     raw_dummy3.parseCommands(CompileDbFile(dummy_path), app);
993     auto cmds = CompileCommandDB(app.data);
994 
995     auto found = cmds.find(dummy_dir ~ "/dir2/file3.cpp");
996     assert(found.length == 1);
997 
998     found.toString.shouldEqual(format("%s/dir2
999   file3.cpp
1000   %s/dir2/file3.cpp
1001   g++ -Idir1 -c -o binary file3.cpp
1002 ", dummy_dir, dummy_dir));
1003 }
1004 
1005 @("Should be a pretty printed search result")
1006 unittest {
1007     auto app = appender!(CompileCommand[])();
1008     raw_dummy2.parseCommands(CompileDbFile(dummy_path), app);
1009     auto cmds = CompileCommandDB(app.data);
1010     auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp");
1011 
1012     found.toString.shouldEqual(format("%s/dir
1013   file2.cpp
1014   %s/dir/file2.cpp
1015   g++ -Idir1 -c -o binary file2.cpp
1016 ", dummy_dir, dummy_dir));
1017 }
1018 
1019 @("Should be a compile command DB with relative path")
1020 unittest {
1021     enum raw = `[
1022     {
1023         "directory": ".",
1024         "command": "g++ -Idir1 -c -o binary file1.cpp",
1025         "file": "file1.cpp"
1026     }
1027     ]`;
1028     auto app = appender!(CompileCommand[])();
1029     raw.parseCommands(CompileDbFile(dummy_path), app);
1030     auto cmds = app.data;
1031 
1032     assert(cmds.length == 1);
1033     (cast(string) cmds[0].directory).shouldEqual(dummy_dir);
1034     (cast(string) cmds[0].file).shouldEqual("file1.cpp");
1035     (cast(string) cmds[0].absoluteFile).shouldEqual(dummy_dir ~ "/file1.cpp");
1036 }
1037 
1038 @("Should be a DB read from a relative path with the contained paths adjusted appropriately")
1039 unittest {
1040     auto app = appender!(CompileCommand[])();
1041     raw_dummy3.parseCommands(CompileDbFile("path/compilation_db.json"), app);
1042     auto cmds = CompileCommandDB(app.data);
1043 
1044     // trusted: constructing a path in memory which is never used for writing.
1045     auto abs_path = () @trusted { return getcwd() ~ "/path"; }();
1046 
1047     auto found = cmds.find(abs_path ~ "/dir2/file3.cpp");
1048     assert(found.length == 1);
1049 
1050     found.toString.shouldEqual(format("%s/dir2
1051   file3.cpp
1052   %s/dir2/file3.cpp
1053   g++ -Idir1 -c -o binary file3.cpp
1054 ", abs_path, abs_path));
1055 }
1056 
1057 @("shall extract arguments, file, directory and output with absolute paths")
1058 unittest {
1059     auto app = appender!(CompileCommand[])();
1060     raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app);
1061     auto cmds = CompileCommandDB(app.data);
1062 
1063     // trusted: constructing a path in memory which is never used for writing.
1064     auto abs_path = () @trusted { return getcwd() ~ "/path"; }();
1065 
1066     auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp"));
1067     assert(found.length == 1);
1068 
1069     found.toString.shouldEqual(format("%s/dir2
1070   file3.cpp
1071   %s/dir2/file3.cpp
1072   file3.o
1073   %s/dir2/file3.o
1074   g++ -Idir1 -c -o binary file3.cpp
1075 ", abs_path, abs_path, abs_path));
1076 }
1077 
1078 @("shall be the compiler flags derived from the arguments attribute")
1079 unittest {
1080     auto app = appender!(CompileCommand[])();
1081     raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app);
1082     auto cmds = CompileCommandDB(app.data);
1083 
1084     // trusted: constructing a path in memory which is never used for writing.
1085     auto abs_path = () @trusted { return getcwd() ~ "/path"; }();
1086 
1087     auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp"));
1088     assert(found.length == 1);
1089 
1090     found[0].parseFlag(defaultCompilerFilter).cflags.shouldEqual([
1091             "-I", buildPath(abs_path, "dir2", "dir1")
1092             ]);
1093 }
1094 
1095 @("shall find the entry based on an output match")
1096 unittest {
1097     auto app = appender!(CompileCommand[])();
1098     raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app);
1099     auto cmds = CompileCommandDB(app.data);
1100 
1101     // trusted: constructing a path in memory which is never used for writing.
1102     auto abs_path = () @trusted { return getcwd() ~ "/path"; }();
1103 
1104     auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o"));
1105     assert(found.length == 1);
1106 
1107     (cast(string) found[0].absoluteFile).shouldEqual(buildPath(abs_path, "dir2", "file3.cpp"));
1108 }
1109 
1110 @("shall parse the compilation database when *arguments* is a json list")
1111 unittest {
1112     auto app = appender!(CompileCommand[])();
1113     raw_dummy5.parseCommands(CompileDbFile("path/compilation_db.json"), app);
1114     auto cmds = CompileCommandDB(app.data);
1115 
1116     // trusted: constructing a path in memory which is never used for writing.
1117     auto abs_path = () @trusted { return getcwd() ~ "/path"; }();
1118 
1119     auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o"));
1120     assert(found.length == 1);
1121 
1122     (cast(string) found[0].absoluteFile).shouldEqual(buildPath(abs_path, "dir2", "file3.cpp"));
1123 }
1124 
1125 @("shall parse the compilation database and find a match via the glob pattern")
1126 unittest {
1127     import std.path : baseName;
1128 
1129     auto app = appender!(CompileCommand[])();
1130     raw_dummy5.parseCommands(CompileDbFile("path/compilation_db.json"), app);
1131     auto cmds = CompileCommandDB(app.data);
1132 
1133     auto found = cmds.find("*/dir2/file3.cpp");
1134     assert(found.length == 1);
1135 
1136     found[0].absoluteFile.baseName.shouldEqual("file3.cpp");
1137 }
1138 
1139 @("shall extract filepath from includes correctly when there is spaces in the path")
1140 unittest {
1141     auto cmd = toCompileCommand("/home", "file.cpp", [
1142             "-I", `"dir with spaces"`, "-I", `\"dir with spaces\"`
1143             ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null);
1144     auto pargs = cmd.get.parseFlag(defaultCompilerFilter);
1145     pargs.cflags.shouldEqual([
1146             "-I", "/home/dir with spaces", "-I", "/home/dir with spaces"
1147             ]);
1148     pargs.includes.shouldEqual([
1149             "/home/dir with spaces", "/home/dir with spaces"
1150             ]);
1151 }
1152 
1153 @("shall handle path with spaces, both as separate string and combined with backslash")
1154 unittest {
1155     auto cmd = toCompileCommand("/project", "file.cpp", [
1156             "-I", `"separate dir/with space"`, "-I", `\"separate dir/with space\"`,
1157             `-I"combined dir/with space"`, `-I\"combined dir/with space\"`,
1158             ], AbsoluteCompileDbDirectory("/project".Path.AbsolutePath), null);
1159     auto pargs = cmd.get.parseFlag(defaultCompilerFilter);
1160     pargs.cflags.shouldEqual([
1161             "-I", "/project/separate dir/with space", "-I",
1162             "/project/separate dir/with space", "-I",
1163             "/project/combined dir/with space", "-I",
1164             "/project/combined dir/with space"
1165             ]);
1166     pargs.includes.shouldEqual([
1167             "/project/separate dir/with space", "/project/separate dir/with space",
1168             "/project/combined dir/with space", "/project/combined dir/with space"
1169             ]);
1170 }
1171 
1172 @("shall handle path with consecutive spaces")
1173 unittest {
1174     auto cmd = toCompileCommand("/project", "file.cpp",
1175             [
1176                 `-I"one space/lots of     space"`,
1177                 `-I\"one space/lots of     space\"`, `-I`,
1178                 `"one space/lots of     space"`, `-I`,
1179                 `\"one space/lots of     space\"`,
1180             ], AbsoluteCompileDbDirectory("/project".Path.AbsolutePath), null);
1181     auto pargs = cmd.get.parseFlag(defaultCompilerFilter);
1182     pargs.cflags.shouldEqual([
1183             "-I", "/project/one space/lots of     space", "-I",
1184             "/project/one space/lots of     space", "-I",
1185             "/project/one space/lots of     space", "-I",
1186             "/project/one space/lots of     space",
1187             ]);
1188     pargs.includes.shouldEqual([
1189             "/project/one space/lots of     space",
1190             "/project/one space/lots of     space",
1191             "/project/one space/lots of     space",
1192             "/project/one space/lots of     space"
1193             ]);
1194 }